package controller

import (
	"errors"
	"fmt"
	"gitee.com/kristas/booting-go/framework/common/util/convert"
	"gitee.com/kristas/booting-go/framework/common/util/reflectx"
	"gitee.com/kristas/booting-go/framework/web"
	"io"
	"reflect"
)

var valueMap = map[reflect.Kind]func(param web.Param, mapper ParamMapper) (interface{}, error){
	reflect.Map:       buildMap,
	reflect.Slice:     buildSlice,
	reflect.String:    buildString,
	reflect.Struct:    buildStruct,
	reflect.Ptr:       buildPtr,
	reflect.Interface: buildInterface,
}

func getValue(param web.Param, mapper ParamMapper) (interface{}, error) {
	if handler, ok := valueMap[param.GetType().Kind()]; ok {
		return handler(param, mapper)
	}
	return buildBaseType(param, mapper)
}

func buildBaseType(param web.Param, mapper ParamMapper) (interface{}, error) {
	src, err := buildString(param, mapper)
	if err != nil {
		return nil, err
	}
	return convert.Convert(src.(string), param.GetType())
}

func buildMap(param web.Param, mapper ParamMapper) (interface{}, error) {
	switch param.GetSource() {
	case web.Form:
		return mapper.GetMapFromForm()
	default:
		return notSupportError(param)
	}
}

func buildSlice(param web.Param, mapper ParamMapper) (interface{}, error) {
	switch param.GetSource() {
	case web.Stream:
		req := mapper.GetRequest()
		bodyReader := req.Body
		buf, _ := io.ReadAll(bodyReader)
		return buf, nil
	case web.Multipart:
		return mapper.GetDataFromMultipart(param.GetName())
	default:
		return notSupportError(param)
	}
}

func buildString(param web.Param, mapper ParamMapper) (interface{}, error) {
	switch param.GetSource() {
	case web.Path:
		return mapper.GetStringFromPath(param.GetName())
	case web.Query:
		return mapper.GetStringFromQuery(param.GetName())
	case web.Header:
		return mapper.GetStringFromHeader(param.GetName())
	case web.Form:
		return mapper.GetStringFromForm(param.GetName())
	case web.Multipart:
		bytes, err := mapper.GetDataFromMultipart(param.GetName())
		return string(bytes), err
	default:
		return notSupportError(param)
	}
}

func buildStruct(param web.Param, mapper ParamMapper) (interface{}, error) {
	switch param.GetSource() {
	case web.Query:
		return buildStructWithGetter(param, mapper.GetStringFromQuery)
	case web.Body:
		return mapper.BindData(param)
	case web.Header:
		return buildStructWithGetter(param, mapper.GetStringFromHeader)
	case web.Path:
		return buildStructWithGetter(param, mapper.GetStringFromPath)
	default:
		return notSupportError(param)
	}
}

func buildPtr(param web.Param, mapper ParamMapper) (interface{}, error) {
	switch param.GetSource() {
	case web.Multipart:
		return mapper.GetMultipartHeader(param.GetName())
	case web.Request:
		return mapper.GetRequest(), nil
	case web.Query:
		return buildStructWithGetter(param, mapper.GetStringFromQuery)
	case web.Body:
		return mapper.BindData(param)
	case web.Header:
		return buildStructWithGetter(param, mapper.GetStringFromHeader)
	case web.Path:
		return buildStructWithGetter(param, mapper.GetStringFromPath)
	default:
		return notSupportError(param)
	}
}

func buildInterface(param web.Param, mapper ParamMapper) (interface{}, error) {
	switch param.GetSource() {
	case web.Multipart:
		return mapper.GetMultipartFile(param.GetName())
	case web.Response:
		return mapper.GetResponseWriter(), nil
	case web.Context:
		return mapper.GetContext(), nil
	default:
		return notSupportError(param)
	}
}

type getter func(name string) (string, error)

func buildStructWithGetter(param web.Param, getter getter) (interface{}, error) {
	v := reflect.New(param.GetType()).Elem()
	err := reflectx.ForEachFieldV2(v.Type(), v, true,
		func(field reflect.StructField, value reflect.Value) error {
			tag := field.Tag.Get("json")
			if tag == "" {
				tag = field.Name
			}
			src, err := getter(tag)
			if err != nil {
				return err
			}
			err = convert.SetValue(src, field.Type, value)
			if err != nil {
				return err
			}
			return nil
		})
	if err != nil {
		return nil, err
	}
	return v.Interface(), nil
}

func notSupportError(param web.Param) (interface{}, error) {
	return nil, errors.New(fmt.Sprintf("param %s in source %s not support", param.GetName(), param.GetSource()))
}
